﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Windows.Media.Media3D;
using LFSRecord2.Events;
using LFSRecord2.Misc;
using LFSRecord2.Model.Media;
using LFSRecord2.Model.Undo;
using LFSRecord2.Model.UserActions;
using LFSRecord2.Structures;
using LFSRecord2.Model.Sound;

namespace LFSRecord2.Model
{
    public class LfsRecordProject : INotifyPropertyChanged, IDisposable
    {
        public string ProjectName { get; set; }
        public string ProjectFilePath { get; set; }
        public double ProjectLength { get; set; }            // Project length in seconds

        private byte _fps = 25;
        public byte FPS
        {
            get { return _fps; }
            set
            {
                _fps = value;
                OnPropertyChanged("FPS");
            }
        }

        public string InSimIP { get; set; }
        public ushort InSimPort { get; set; }
        public string InSimAdminPassword { get; set; }

        public bool RecordFullScreenOverride { get; set; }
        public byte RecordStopHotkey { get; set; }
        public ushort RecordingWidth { get; set; }
        public ushort RecordingHeight { get; set; }
        public bool RecordHide2D { get; set; }

        public bool Move3DAxesInvert { get; set; }

        public bool Edited { get; set; }

        ObservableCollection<CompLayerBase> _layers;
        public ObservableCollection<CompLayerBase> Layers
        {
            get
            {
                if (_layers == null)
                    _layers = new ObservableCollection<CompLayerBase>();
                return _layers;
            }
        }

        Object _audioLock = new Object();
        ObservableCollection<AudioLayer> _audioLayers;
        public ObservableCollection<AudioLayer> AudioLayers
        {
            get
            {
                if (_audioLayers == null)
                    _audioLayers = new ObservableCollection<AudioLayer>();
                return _audioLayers;
            }
        }

        ObservableCollection<MediaBase> _mediaObjects;
        public ObservableCollection<MediaBase> MediaObjects
        {
            get
            {
                if (_mediaObjects == null)
                    _mediaObjects = new ObservableCollection<MediaBase>();
                return _mediaObjects;
            }
        }

        private List<KeyFrameCopy> KeyFrameCopyBuffer = new List<KeyFrameCopy>();

        public KeyFrameInterpolationTypes DefaultInterpolationType = KeyFrameInterpolationTypes.Bezier;

        public event PropertyChangedEventHandler PropertyChanged;
        protected virtual void OnPropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler handler = this.PropertyChanged;
            if (handler != null)
            {
                var e = new PropertyChangedEventArgs(propertyName);
                handler(this, e);
            }
        }

        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
                PropertyChanged = null;
        }
        
        public LfsRecordProject()
        {
            ProjectName = "";
            ProjectFilePath = "";
            ProjectLength = 60.0d;
            
            InSimIP = "127.0.0.1";
            InSimPort = 29999;
            InSimAdminPassword = "";

            RecordFullScreenOverride = false;
            RecordStopHotkey = 106;                     // default vkCode of 106 (*)
            RecordingWidth = 800;
            RecordingHeight = 600;
            RecordHide2D = true;
        }

        public LfsRecordProject(string projectName) : this()
        {
            ProjectName = projectName;
        }

        public void SetDefaultValues()
        {
            ProjectFilePath = "";
            ProjectLength = 60.0d;
            FPS = 25;
            Edited = false;
        }

        /// <summary>
        /// Deletes ALL user data in the project. So that is layers/properties/keyframes/media files.
        /// </summary>
        public void deleteAll()
        {
            // Layers
            for (int a = 0; a < Layers.Count; a++)
            {
                DeleteLayer(Layers[a]);
                a--;
            }

            // AudioLayers
            for (int a = 0; a < AudioLayers.Count; a++)
            {
                DeleteLayer(AudioLayers[a]);
                a--;
            }

            // Media objects
            for (int a = 0; a < MediaObjects.Count; a++)
            {
                DeleteMediaObject(MediaObjects[a].FilePath);
                a--;
            }

            // buffers
            KeyFrameCopyBuffer.Clear();
        }

        public void DeleteSelectedMediaObjects()
        {
            for (int a = 0; a < MediaObjects.Count; a++)
            {
                if (MediaObjects[a].IsSelected)
                {
                    DeleteMediaObject(MediaObjects[a].FilePath);
                    a--;
                }
            }
        }

        public void DeleteSelectedLayers()
        {
            for (int a = 0; a < Layers.Count; a++)
            {
                if (Layers[a].IsSelected)
                {
                    LfsRecordController.RecordUserAction(new UserActionDeleteLayer()
                    {
                        LayerIndex = Layers[a].LayerIndex,
                        Layer = LfsRecordController.project.Layers[Layers[a].LayerIndex].DataStructure()
                    });
                    DeleteLayer(Layers[a]);
                    a--;
                }
            }

            for (int a = 0; a < AudioLayers.Count; a++)
            {
                if (AudioLayers[a].IsSelected)
                {
                    LfsRecordController.RecordUserAction(new UserActionDeleteAudioLayer()
                    {
                        LayerIndex = AudioLayers[a].LayerIndex,
                        Layer = AudioLayers[a].DataStructure()
                    });
                    DeleteLayer(AudioLayers[a]);
                    a--;
                }
            }

            if (GetActiveCameraLayerIndex() == -1)
            {
                SetFirstCameraToActive();
                ServicesFactory.EventService.GetEvent<CompLayerPropertyChangedEvent>().Publish(new CompLayerPropertyChangedEventArgs() { Sender = this, PropertyType = "Layer", UpdateCurrentValues = true });
            }
        }

        public void DeleteLayer(CompLayerBase layer)
        {
            CompLayerPropertyBase prop;

            // Check if layer has a parent
            if (layer.HasParentLayer)
            {
                layer.ParentLayer = null;
            }

            // Remove any possible property bindings to this layer
            for (int a = 0; a < Layers.Count; a++)
            {
                // Check properties of to be deleted layer for bindings
                if (a == layer.LayerIndex)
                {
                    for (int b = 0; b < Layers[a].Properties.Count; b++)
                    {
                        //Debug.WriteLine("Removing bound property on prop {0} layer {1}", b, a);
                        Layers[a].Properties[b].BoundProperty = null;
                    }
                }
                else
                {
                    // Check if other layers have the deleted layer parented
                    if (Layers[a].HasParentLayer && Layers[a].ParentLayer.LayerIndex == layer.LayerIndex)
                    {
                        Layers[a].ParentLayer = null;
                    }
                }

                // Check properties of other layers for bindings to deleted layer
                for (int b = 0; b < Layers[a].Properties.Count; b++)
                {
                    prop = Layers[a].Properties[b];
                    if (prop.BindingPropertyIndex.LayerIndex == layer.LayerIndex)
                    {
                        //Debug.WriteLine("Removing bound property on prop {0} layer {1}", b, a);
                        prop.BoundProperty = null;
                        //prop.RemoveCurrentValueListener();
                    }
                }
            }

            // Remove the layer
            for (int a = 0; a < Layers.Count; a++)
            {
                if (Layers[a] == layer)
                {
                    for (int b = 0; b < Layers[a].Properties.Count; b++)
                    {
                        Layers[a].Properties[b].Dispose();
                        Layers[a].Properties[b] = null;
                    }

                    Layers[a].Dispose();
                    Layers.RemoveAt(a);
                    break;
                }
            }
            RewriteLayerIndices();

            if (GetActiveCameraLayerIndex() == -1)
            {
                SetFirstCameraToActive();
                ServicesFactory.EventService.GetEvent<CompLayerPropertyChangedEvent>().Publish(new CompLayerPropertyChangedEventArgs() { Sender = this, PropertyType = "Layer", UpdateCurrentValues = true });
            }
        }

        public void DeleteLayer(AudioLayer layer)
        {
            lock (_audioLock)
            {
                for (int a = 0; a < AudioLayers.Count; a++)
                {
                    if (AudioLayers[a] == layer)
                    {
                        AudioLayers[a].Dispose();
                        AudioLayers.RemoveAt(a);
                        break;
                    }
                }
                RewriteAudioLayerIndices();
            }
        }

        public void ReinstateLayer(LayerData Layer)
        {
            CompLayerBase l = null;
            switch (Layer.LayerType)
            {
                case CompLayerTypes.LookCamera:
                    l = AddLookCamera(Layer.LayerName, Layer.LayerIndex);
                    break;
                case CompLayerTypes.FreeCamera:
                    l = AddFreeCamera(Layer.LayerName, Layer.LayerIndex);
                    break;
                case CompLayerTypes.Null:
                    l = AddNull(Layer.LayerName, Layer.LayerIndex);
                    break;
                case CompLayerTypes.ReplayControl:
                    l = AddReplayControl(Layer.LayerName, Layer.LayerIndex);
                    break;
                case CompLayerTypes.ReplayData:
                    l = AddReplayData(Layer.LayerName, Layer.LayerIndex);
                    break;
            }

            if (l == null)
                return;

            DeselectAll();
            l.UniqueID = Layer.UniqueID;
            l.IsCollapsed = Layer.IsCollapsed;
            l.IsSelected = true;
            if (Layer.IsActiveCamera)
            {
                Layers[GetActiveCameraLayerIndex()].IsActiveCamera = false;
                l.IsActiveCamera = Layer.IsActiveCamera;
            }
            
            // Populate the layer with properties and keyframes
            CompLayerPropertyBase prop;
            CompLayerPropertyDouble propDouble;
            CompLayerPropertyPoint3D propPoint3D;
            CompLayerPropertyString propString;
            CompLayerPropertyRDIdent propRDIdent;
            for (int a = 0; a< l.Properties.Count; a++)
            {
                // Generic property attributes
                prop = l.Properties[a];
                prop.Initializing = true;
                prop.IsSelected = Layer.Properties[a].IsSelected;
                prop.IsAnimated = Layer.Properties[a].IsAnimated;
                prop.ValueLock = Layer.Properties[a].ValueLock;
                prop.Initializing = false;
                prop.BoundPropertyIgnoreParent = Layer.Properties[a].BoundPropertyIgnoreParent;
                if (Layer.Properties[a].BindingPropertyIndex.LayerIndex != -1 && Layer.Properties[a].BindingPropertyIndex.PropertyIndex != -1)
                {
                    // Property has a value binding (and will have no keyframes)
                    prop.BoundProperty = GetProperty(Layer.Properties[a].BindingPropertyIndex);
                }

                // Specific attributes (keyframes & current values)
                if (l.Properties[a].PropertyValueType == LayerPropertyValueTypes.Point3D)
                {
                    propPoint3D = (CompLayerPropertyPoint3D)l.Properties[a];

                    // Keyframes
                    for (int b = 0; b < Layer.Properties[a].NumKeyFrames; b++)
                    {
                        propPoint3D.ReinstateKeyFrame(Layer.Properties[a].KeyFrames[b]);
                    }

                    // Current values
                    propPoint3D.CurrentXValue = ((PropertyPoint3DData)Layer.Properties[a]).CurrentXValue;
                    propPoint3D.CurrentYValue = ((PropertyPoint3DData)Layer.Properties[a]).CurrentYValue;
                    propPoint3D.CurrentZValue = ((PropertyPoint3DData)Layer.Properties[a]).CurrentZValue;
                }
                else if (l.Properties[a].PropertyValueType == LayerPropertyValueTypes.Double)
                {
                    propDouble = (CompLayerPropertyDouble)l.Properties[a];

                    // Keyframes
                    for (int b = 0; b < Layer.Properties[a].NumKeyFrames; b++)
                    {
                        propDouble.ReinstateKeyFrame(Layer.Properties[a].KeyFrames[b]);
                    }

                    // Current values
                    propDouble.CurrentValue = ((PropertyDoubleData)Layer.Properties[a]).CurrentValue;
                }
                else if (l.Properties[a].PropertyValueType == LayerPropertyValueTypes.String)
                {
                    propString = (CompLayerPropertyString)l.Properties[a];

                    // Keyframes
                    for (int b = 0; b < Layer.Properties[a].NumKeyFrames; b++)
                    {
                        propString.ReinstateKeyFrame(Layer.Properties[a].KeyFrames[b]);
                    }

                    // Current values
                    propString.CurrentValue = ((PropertyStringData)Layer.Properties[a]).CurrentValue;
                }
                else if (l.Properties[a].PropertyValueType == LayerPropertyValueTypes.RDIdent)
                {
                    propRDIdent = (CompLayerPropertyRDIdent)l.Properties[a];

                    // Keyframes
                    for (int b = 0; b < Layer.Properties[a].NumKeyFrames; b++)
                    {
                        propRDIdent.ReinstateKeyFrame(Layer.Properties[a].KeyFrames[b]);
                    }

                    // Current values
                    propRDIdent.CurrentValue = ((PropertyRDIdentData)Layer.Properties[a]).CurrentValue;
                }
            }

            // Did the layer have a parent?
        }

        public void ReinstateLayer(AudioLayerData layer)
        {
            lock (_audioLock)
            {
                AudioLayer l = AddAudio(layer.LayerName, layer.LayerIndex);
                DeselectAll();
                l.UniqueID = layer.UniqueID;
                l.IsCollapsed = layer.IsCollapsed;
                l.IsSelected = true;

                // Populate Audio Regions
                for (int a = 0; a < layer.NumRegions; a++)
                {
                    l.ReinstateAudioRegion(layer.AudioRegions[a]);
                }

                // Populate properties (when we have used them)
            }
        }

        public void RewriteLayerIndices()
        {
            int a = 0;
            foreach (CompLayerBase layer in Layers)
            {
                layer.LayerIndex = (short)a++;
            }
        }

        public void RewriteAudioLayerIndices()
        {
            int a = 0;
            lock (_audioLock)
            {
                foreach (AudioLayer layer in AudioLayers)
                {
                    layer.LayerIndex = (short)a++;
                }
            }
        }

        public CompLayerPropertyBase GetProperty(LayerPropIndex bindingPropIndex)
        {
            if (bindingPropIndex.LayerIndex < Layers.Count &&
                bindingPropIndex.PropertyIndex < Layers[bindingPropIndex.LayerIndex].Properties.Count)
                return Layers[bindingPropIndex.LayerIndex].Properties[bindingPropIndex.PropertyIndex];
            return null;
        }

        public int GetActiveCameraLayerIndex()
        {
            int index = -1;
            for (int a = 0; a < Layers.Count; a++)
            {
                if (Layers[a].LayerType == CompLayerTypes.FreeCamera)
                {
                    if (((CompLayerFreeCamera)Layers[a]).IsActiveCamera)
                    {
                        index = a;
                        break;
                    }
                }
                else if (Layers[a].LayerType == CompLayerTypes.LookCamera)
                {
                    if (((CompLayerLookCamera)Layers[a]).IsActiveCamera)
                    {
                        index = a;
                        break;
                    }
                }
            }

            //Debug.WriteLine("Active camera at : " + index);

            return index;
        }

        public void SetFirstCameraToActive()
        {
            bool haveSet = false;
            for (int a = 0; a < Layers.Count; a++)
            {
                if (Layers[a].LayerType == CompLayerTypes.FreeCamera)
                {
                    Debug.WriteLine("Have set active a free camera");
                    ((CompLayerFreeCamera)Layers[a]).IsActiveCamera = (haveSet) ? false : true;
                    haveSet = true;
                }
                else if (Layers[a].LayerType == CompLayerTypes.LookCamera)
                {
                    Debug.WriteLine("Have set active a look camera");
                    ((CompLayerLookCamera)Layers[a]).IsActiveCamera = (haveSet) ? false : true;
                    haveSet = true;
                }
            }
        }

        public void DeleteKeyFrameViaIndex(int layerIndex, int propertyIndex, int keyFrameIndex, bool postProcess = true)
        {
            CustomKeyFrame kf;
            for (int a = 0; a < Layers[layerIndex].Properties[propertyIndex].KeyFrames.Count; a++)
            {
                kf = Layers[layerIndex].Properties[propertyIndex].KeyFrames[a];
                if (kf.KeyFrameIndex == keyFrameIndex)
                {
                    Layers[layerIndex].Properties[propertyIndex].KeyFrames[a].Dispose();
                    Layers[layerIndex].Properties[propertyIndex].KeyFrames.RemoveAt(a);
                    break;
                }
            }

            if (postProcess)
                Layers[layerIndex].Properties[propertyIndex].PostProcess();
        }

        public void DeleteSelectedKeyFrames()
        {
            bool haveDeleted;

            foreach (CompLayerBase l in Layers)
            {
                foreach (CompLayerPropertyBase prop in l.Properties)
                {
                    haveDeleted = false;
                    for (int a = 0; a < prop.KeyFrames.Count; a++)
                    {
                        if (prop.KeyFrames[a].IsSelected)
                        {
                            prop.KeyFrames[a].Dispose();
                            prop.KeyFrames.RemoveAt(a);
                            a--;
                            haveDeleted = true;
                        }
                    }

                    if (haveDeleted)
                    {
                        prop.PostProcess();
                        prop.IsEdited = true;
                    }
                }
            }
            ServicesFactory.EventService.GetEvent<CompLayerPropertyChangedEvent>().Publish(new CompLayerPropertyChangedEventArgs() { Sender = this, PropertyType = "Value", UpdateCurrentValues = true });
        }

        public void DeleteSelectedAudioRegions()
        {
            lock (_audioLock)
            {
                foreach (AudioLayer l in AudioLayers)
                {
                    for (int a = 0; a < l.AudioRegions.Count; a++)
                    {
                        if (l.AudioRegions[a].IsSelected)
                        {
                            l.AudioRegions[a].Dispose();
                            l.AudioRegions.RemoveAt(a);
                            a--;
                        }
                    }
                    //l.SortRegions();
                    l.WriteRegionIndexes();
                }
            }
        }

        public LfsRecordProjectSettings GetSettingsObject()
        {
            LfsRecordProjectSettings p = new LfsRecordProjectSettings();
            p.ProjectName = ProjectName;
            p.ProjectFilePath = ProjectFilePath;
            p.ProjectLength = ProjectLength;
            p.FPS = FPS;
            
            p.InSimIP = InSimIP;
            p.InSimPort = InSimPort;
            p.InSimAdminPassword = InSimAdminPassword;

            p.RecordFullScreenOverride = RecordFullScreenOverride;
            p.RecordStopHotkey = RecordStopHotkey;
            p.RecordingWidth = RecordingWidth;
            p.RecordingHeight = RecordingHeight;
            p.RecordHide2D = RecordHide2D;

            p.Move3DAxesInvert = Move3DAxesInvert;

            p.AudioOutputDevices = new Collection<WaveOutCaps>(WaveIO.EnumOutDevices());
            p.AudioOutputDevice = LfsRecordController.Mixer.CurrentWaveOutDevice;
            p.SampleRate = LfsRecordController.Mixer.SampleRate;

            return p;
        }

        public CompLayerLookCamera AddLookCamera(string layerName = "", int insertIndex = -1)
        {
            //Debug.WriteLine("Adding Camera");
            CompLayerLookCamera myCam;
            if (layerName.Length > 0)
                myCam = new CompLayerLookCamera((short)Layers.Count, layerName);
            else
                myCam = new CompLayerLookCamera((short)Layers.Count, "LookCamera" + NumLayersOfType(CompLayerTypes.LookCamera));

            if (insertIndex == -1 || insertIndex == Layers.Count)
                Layers.Add(myCam);
            else
                Layers.Insert(insertIndex, myCam);
            RewriteLayerIndices();

            DeselectAll();
            myCam.IsSelected = true;

            //ServicesFactory.EventService.GetEvent<FrameIndexChangedEvent>().Publish(new FrameIndexChangedEventArgs() { Sender = this, NewFrameIndex = LFSRecordController.FrameIndex, UpdateCurrentValues = true });

            if (GetActiveCameraLayerIndex() == -1)
            {
                myCam.IsActiveCamera = true;
                ServicesFactory.EventService.GetEvent<CompLayerPropertyChangedEvent>().Publish(new CompLayerPropertyChangedEventArgs() { Sender = this, PropertyType = "Layer", UpdateCurrentValues = true });
            }

            return myCam;
        }

        public CompLayerFreeCamera AddFreeCamera(string layerName = "", int insertIndex = -1)
        {
            //Debug.WriteLine("Adding Camera");
            CompLayerFreeCamera myCam;
            if (layerName.Length > 0)
                myCam = new CompLayerFreeCamera((short)Layers.Count, layerName);
            else
                myCam = new CompLayerFreeCamera((short)Layers.Count, "FreeCamera" + NumLayersOfType(CompLayerTypes.FreeCamera));

            if (insertIndex == -1 || insertIndex == Layers.Count)
                Layers.Add(myCam);
            else
                Layers.Insert(insertIndex, myCam);
            RewriteLayerIndices();

            DeselectAll();
            myCam.IsSelected = true;

            //ServicesFactory.EventService.GetEvent<FrameIndexChangedEvent>().Publish(new FrameIndexChangedEventArgs() { Sender = this, NewFrameIndex = LFSRecordController.FrameIndex, UpdateCurrentValues = true });

            if (GetActiveCameraLayerIndex() == -1)
            {
                myCam.IsActiveCamera = true;
                ServicesFactory.EventService.GetEvent<CompLayerPropertyChangedEvent>().Publish(new CompLayerPropertyChangedEventArgs() { Sender = this, PropertyType = "Layer", UpdateCurrentValues = true });
            }

            return myCam;
        }

        public CompLayerNull AddNull(string layerName = "", int insertIndex = -1)
        {
            //Debug.WriteLine("Adding Null");
            CompLayerNull myNull;
            if (layerName.Length > 0)
                myNull = new CompLayerNull((short)Layers.Count, layerName);
            else
                myNull = new CompLayerNull((short)Layers.Count, "Null" + NumLayersOfType(CompLayerTypes.Null));

            if (insertIndex == -1 || insertIndex == Layers.Count)
                Layers.Add(myNull);
            else
                Layers.Insert(insertIndex, myNull);
            RewriteLayerIndices();

            DeselectAll();
            myNull.IsSelected = true;

            ServicesFactory.EventService.GetEvent<FrameIndexChangedEvent>().Publish(new FrameIndexChangedEventArgs() { Sender = this, NewFrameIndex = LfsRecordController.FrameIndex, UpdateCurrentValues = true });
            
            return myNull;
        }

        public bool CreateNullFromReplayData(CompLayerReplayData layer, int frameThinning)
        {
            Debug.WriteLine("Converting RD Layer to Null Layer.");

            // Get first RDIdent
            RDIdent rdIdent = layer.FirstRDIdent();
            if (rdIdent.ReplayFilePath.Length == 0)
                return false;

            // Create a new null layer
            CompLayerNull myNull = new CompLayerNull((short)Layers.Count, rdIdent.PlayerName);
            LfsRecordController.RecordUserAction(new UserActionAddLayer()
            {
                LayerType = myNull.LayerType,
                LayerIndex = myNull.LayerIndex,
                LayerName = myNull.LayerName
            });
            DeselectAll();
            myNull.IsSelected = true;

            // Prepare variables that we need for conversion
            bool doThinning;
            CustomPoint3DKeyFrame kfp;
            CustomDoubleKeyFrame kfd;
            CompLayerPropertyPoint3DRD propPos = (CompLayerPropertyPoint3DRD)layer.Properties[(int)CompLayerReplayData.PropertyTypes.Position];
            CompLayerPropertyDoubleRD propHeading = (CompLayerPropertyDoubleRD)layer.Properties[(int)CompLayerReplayData.PropertyTypes.Heading];
            CompLayerPropertyDouble propTimeOffset = (CompLayerPropertyDouble)layer.Properties[(int)CompLayerReplayData.PropertyTypes.TimeOffset];

            // If timeoffset property has a bound property, we want to get the parent, because we need to know about real keyframes
            while (propTimeOffset.HasBoundProperty)
            {
                propTimeOffset = (CompLayerPropertyDouble)propTimeOffset.BoundProperty;
            }

            // Go from frame 0 to end of project to convert entire timeline
            int projectFrames = (int)Math.Ceiling(ProjectLength * FPS);
            for (int frameNum = 0; frameNum <= projectFrames; frameNum++)
            {
                // Position
                kfp = new CustomPoint3DKeyFrame(frameNum, (Point3D)propPos.GetValueAtFrame(frameNum));
                kfp.InterpolationTypeIn = KeyFrameInterpolationTypes.Linear;
                kfp.InterpolationTypeOut = KeyFrameInterpolationTypes.Linear;
                ((CompLayerPropertyPoint3D)myNull.Properties[(int)CompLayerNull.PropertyTypes.Position]).AddKeyFrame(kfp, false);
                //Debug.WriteLine("MCI NULL KF index : {0}", kfp.KeyFrameIndex);
                LfsRecordController.RecordUserAction(new UserActionAddKeyframe()
                {
                    LayerIndex = myNull.LayerIndex,
                    PropertyIndex = (int)CompLayerNull.PropertyTypes.Position,
                    KeyFrame = kfp.DataStructure()
                });

                // Heading
                kfd = new CustomDoubleKeyFrame(frameNum, (double)propHeading.GetValueAtFrame(frameNum));
                ((CompLayerPropertyDouble)myNull.Properties[(int)CompLayerNull.PropertyTypes.RotateZ]).AddKeyFrame(kfd);
                LfsRecordController.RecordUserAction(new UserActionAddKeyframe()
                {
                    LayerIndex = myNull.LayerIndex,
                    PropertyIndex = (int)CompLayerNull.PropertyTypes.RotateZ,
                    KeyFrame = kfd.DataStructure()
                });

                // Frame thinning and replay time offset change accuracy check
                doThinning = true;
                if (frameThinning > 0)
                {
                    if (frameNum + frameThinning < projectFrames)
                    {
                        // Check if current framenum was exactly on a keyframe - if so, we don't want to thin next keyframe
                        if (propTimeOffset.GetKeyAtTime(frameNum + 1) != null)
                        {
                            doThinning = false;
                        }
                        else
                        {
                            // Check if a (next) keyframe falls right on a thinned keyframe - if so, we want to go to that keyframe in next loop
                            for (int a = 0; a < propTimeOffset.KeyFrames.Count; a++)
                            {
                                if (propTimeOffset.KeyFrames[a].FrameNum > frameNum && propTimeOffset.KeyFrames[a].FrameNum <= frameNum + frameThinning)
                                {
                                    doThinning = false;
                                    frameNum = propTimeOffset.KeyFrames[a].FrameNum - 2;
                                    break;
                                }
                            }
                        }

                        if (doThinning)
                            frameNum += frameThinning;
                    }
                    else if (frameNum < projectFrames - 1)
                    {
                        frameNum = projectFrames - 1;
                    }
                }
            }
            myNull.Properties[(int)CompLayerNull.PropertyTypes.Position].IsAnimated = true;
            myNull.Properties[(int)CompLayerNull.PropertyTypes.RotateZ].IsAnimated = true;
            ((CompLayerPropertyPoint3D)myNull.Properties[(int)CompLayerNull.PropertyTypes.Position]).populatePoint3DCache();

            // Add it to the layers array
            Layers.Add(myNull);
            RewriteLayerIndices();

            return true;
        }

        public CompLayerReplayControl AddReplayControl(string layerName = "", int insertIndex = -1)
        {
            if (NumLayersOfType(CompLayerTypes.ReplayControl) > 0)
                return null;

            CompLayerReplayControl myReplayControl;
            if (layerName.Length > 0)
                myReplayControl = new CompLayerReplayControl((short)Layers.Count, layerName);
            else
                myReplayControl = new CompLayerReplayControl((short)Layers.Count, "ReplayControl");

            if (insertIndex == -1 || insertIndex == Layers.Count)
                Layers.Add(myReplayControl);
            else
                Layers.Insert(insertIndex, myReplayControl);
            RewriteLayerIndices();

            DeselectAll();
            myReplayControl.IsSelected = true;

            ServicesFactory.EventService.GetEvent<FrameIndexChangedEvent>().Publish(new FrameIndexChangedEventArgs() { Sender = this, NewFrameIndex = LfsRecordController.FrameIndex, UpdateCurrentValues = true });

            return myReplayControl;
        }

        public CompLayerReplayData AddReplayData(string layerName = "", int insertIndex = -1)
        {
            CompLayerReplayData myReplayData;
            if (layerName.Length > 0)
                myReplayData = new CompLayerReplayData((short)Layers.Count, layerName);
            else
                myReplayData = new CompLayerReplayData((short)Layers.Count, "ReplayData" + NumLayersOfType(CompLayerTypes.ReplayData));

            if (insertIndex == -1 || insertIndex == Layers.Count)
                Layers.Add(myReplayData);
            else
                Layers.Insert(insertIndex, myReplayData);
            RewriteLayerIndices();

            DeselectAll();
            myReplayData.IsSelected = true;

            ServicesFactory.EventService.GetEvent<FrameIndexChangedEvent>().Publish(new FrameIndexChangedEventArgs() { Sender = this, NewFrameIndex = LfsRecordController.FrameIndex, UpdateCurrentValues = true });

            return myReplayData;
        }

        public AudioLayer AddAudio(string layerName = "", int insertIndex = -1)
        {
            //Debug.WriteLine("Adding Audio");
            AudioLayer myAudio;
            if (layerName.Length > 0)
                myAudio = new AudioLayer((short)AudioLayers.Count, layerName, _audioLock);
            else
                myAudio = new AudioLayer((short)AudioLayers.Count, "Audio" + NumLayersOfType(CompLayerTypes.Audio), _audioLock);

            if (insertIndex == -1 || insertIndex == AudioLayers.Count)
                AudioLayers.Add(myAudio);
            else
                AudioLayers.Insert(insertIndex, myAudio);
            RewriteLayerIndices();

            DeselectAll();
            myAudio.IsSelected = true;

            ServicesFactory.EventService.GetEvent<FrameIndexChangedEvent>().Publish(new FrameIndexChangedEventArgs() { Sender = this, NewFrameIndex = LfsRecordController.FrameIndex, UpdateCurrentValues = true });

            return myAudio;
        }

        public bool HasReplay()
        {
            foreach (CompLayerBase layer in Layers)
            {
                if (layer.LayerType == CompLayerTypes.ReplayControl)
                {
                    return ((CompLayerReplayControl)layer).HasReplay();
                }
            }
            return false;
        }

        public ReplayRecordInfo GetCurrentReplayInfo()
        {
            ReplayRecordInfo rInfo = GetCurrentReplayEvent();
            foreach (CompLayerBase layer in Layers)
            {
                if (layer.LayerType == CompLayerTypes.ReplayControl)
                {
                    rInfo.HasReplayNameEvent = false;
                    if (rInfo.ReplayName.Length == 0)
                        rInfo.ReplayName = (string)layer.Properties[(int)CompLayerReplayControl.PropertyTypes.ReplayName].GetValueAtFrame(LfsRecordController.FrameIndex);
                    else
                        rInfo.HasReplayNameEvent = true;

                    rInfo.HasReplayOffsetEvent = false;
                    if (rInfo.ReplayOffset == -1)
                        rInfo.ReplayOffset = (double)layer.Properties[(int)CompLayerReplayControl.PropertyTypes.TimeOffset].GetValueAtFrame(LfsRecordController.FrameIndex);
                    else
                        rInfo.HasReplayOffsetEvent = true;
                    return rInfo;
                }
            }
            return rInfo;
        }

        public ReplayRecordInfo GetCurrentReplayEvent()
        {
            CustomKeyFrame kf;
            ReplayRecordInfo rInfo = new ReplayRecordInfo() { ReplayName = "", ReplayOffset = -1 };
            foreach (CompLayerBase layer in Layers)
            {
                if (layer.LayerType == CompLayerTypes.ReplayControl)
                {
                    kf = layer.Properties[(int)CompLayerReplayControl.PropertyTypes.ReplayName].GetKeyAtTime(LfsRecordController.FrameIndex);
                    if (kf != null)
                        rInfo.ReplayName = ((CustomStringKeyFrame)kf).Value;
                    kf = layer.Properties[(int)CompLayerReplayControl.PropertyTypes.TimeOffset].GetKeyAtTime(LfsRecordController.FrameIndex);
                    if (kf != null)
                        rInfo.ReplayOffset = ((CustomDoubleKeyFrame)kf).Value;
                    break;
                }
            }
            return rInfo;
        }

        public int NumLayersOfType(CompLayerTypes type)
        {
            int c = 0;
            if (type == CompLayerTypes.Audio)
            {
                foreach (AudioLayer l in AudioLayers)
                {
                    if (l.LayerType == type)
                        c++;
                }
            }
            else
            {
                foreach (CompLayerBase l in Layers)
                {
                    if (l.LayerType == type)
                        c++;
                }
            }
            return c;
        }

        public int NumMediaOfType(MediaTypes type)
        {
            int c = 0;
            foreach (MediaBase mb in MediaObjects)
            {
                if (mb.MediaType == type)
                    c++;
            }
            return c;
        }

        public int LastProjectKeyFrameNum()
        {
            int lastFrameNum = 0;
            foreach (CompLayerBase l in Layers)
            {
                foreach (CompLayerPropertyBase prop in l.Properties)
                {
                    if (prop.KeyFrames.Count > 0 && prop.KeyFrames[prop.KeyFrames.Count - 1].FrameNum > lastFrameNum)
                        lastFrameNum = prop.KeyFrames[prop.KeyFrames.Count - 1].FrameNum;
                }
            }

            return lastFrameNum;
        }

        public void UpdatedCurrentValuesToZero()
        {
            foreach (CompLayerBase l in Layers)
            {
                l.UpdatedCurrentValues = 0;
            }
        }

        public void LayerPostProcess()
        {
            foreach (CompLayerBase l in Layers)
            {
                if (l.UpdatedCurrentValues == 0)
                    continue;

                foreach (CompLayerPropertyBase prop in l.Properties)
                {
                    prop.PostProcess();
                }

                l.SetCurrentTransformMatrix();
                l.DoneUpdatingCurrentValues = !l.DoneUpdatingCurrentValues;
            }

            foreach (AudioLayer l in AudioLayers)
            {
                l.SortRegions();
                l.WriteRegionIndexes();
            }
        }

        public void SetCurrentValues(int frame)
        {
            // First pass - set UpdatedCurrentValues of each layer (and set the actual currentvalues themselves)
            foreach (CompLayerBase l in Layers)
            {
                l.SetCurrentValues(frame);
            }

            // Second pass - trigger DoneUpdatingCurrentValues for those layers that need it
            CompLayerBase parentLayer;
            bool skip;
            foreach (CompLayerBase l in Layers)
            {
                // If parent is going to trigger me (or already has), skip me
                skip = false;
                parentLayer = l.ParentLayer;
                while (parentLayer != null)
                {
                    if (parentLayer.UpdatedCurrentValues > 0)
                    {
                        skip = true;
                        break;
                    }
                    parentLayer = parentLayer.ParentLayer;
                }
                if (skip)
                    continue;

                //if (l.UpdatedCurrentValues > 0)
                //{
                    l.DoneUpdatingCurrentValues = !l.DoneUpdatingCurrentValues;
                //}
            }
        }

        public void SetKeyWalkerToggles()
        {
            foreach (CompLayerBase l in Layers)
            {
                foreach (CompLayerPropertyBase prop in l.Properties)
                {
                    prop.SetHasPreFrames();
                    prop.SetHasPostFrames();
                    prop.SetHasCurrentFrame();
                }
            }
        }

        public CompLayerBase GetLayerByName(string layerName)
        {
            foreach (CompLayerBase layer in Layers)
            {
                if (layer.LayerName == layerName)
                    return layer;
            }
            return null;
        }

        public void SetActiveCameraValues(LfsCamera cam)
        {
            SetActiveCameraValues(cam, false);
        }
        public void SetActiveCameraValues(LfsCamera cam, bool recordUserActions)
        {
            Matrix3D mat;
            Point3D p;
            CompLayerPropertyPoint3D propPoint3D;
            CompLayerPropertyDouble propDouble;
            CustomPoint3DKeyFrame kfp;
            CustomDoubleKeyFrame kfd;
            double pitch, heading;

            foreach (CompLayerBase l in Layers)
            {
                if (l.LayerType == CompLayerTypes.LookCamera &&
                    ((CompLayerLookCamera)l).IsActiveCamera)
                {
                    CompLayerLookCamera cl = (CompLayerLookCamera)l;
                    propPoint3D = (CompLayerPropertyPoint3D)cl.Properties[(int)CompLayerLookCamera.PropertyTypes.Position];
                    mat = cl.GetParentedTransformMatrixAtFrame(LfsRecordController.FrameIndex, false);
                    mat.Invert();

                    p = mat.Transform(new Point3D(cam.PosX, cam.PosY, cam.PosZ));
                    if (propPoint3D.CurrentXValue != p.X ||
                        propPoint3D.CurrentYValue != p.Y ||
                        propPoint3D.CurrentZValue != p.Z)
                    {
                        propPoint3D.CurrentXValue = p.X;
                        propPoint3D.CurrentYValue = p.Y;
                        propPoint3D.CurrentZValue = p.Z;

                        kfp = (CustomPoint3DKeyFrame)propPoint3D.OnCurrentValueChange();
                        if (kfp != null && recordUserActions)
                        {
                            // New keyframe was created
                            kfp.IsSelected = true;

                            LfsRecordController.RecordUserAction(new UserActionAddKeyframe()
                                {
                                    LayerIndex = kfp.ParentLayer.LayerIndex,
                                    PropertyIndex = kfp.ParentProperty.PropertyIndex,
                                    KeyFrame = kfp.DataStructure()
                                }, 
                                true);
                        }
                        else if (recordUserActions)
                        {
                            // Either a keyframe or plain current values have been adjusted. Figure out which one first
                            // actually no, we do not records keyframetranslate action here. We do that in viewport.
                            // we just needed to know about the keyframe that was added (and related possible neighbour bezier adjustments)
                        }
                    }

                    propPoint3D = (CompLayerPropertyPoint3D)cl.Properties[(int)CompLayerLookCamera.PropertyTypes.LookAt];
                    p = mat.Transform(new Point3D(cam.LookAt.X, cam.LookAt.Y, cam.LookAt.Z));
                    if (propPoint3D.CurrentXValue != p.X ||
                        propPoint3D.CurrentYValue != p.Y ||
                        propPoint3D.CurrentZValue != p.Z)
                    {
                        propPoint3D.CurrentXValue = p.X;
                        propPoint3D.CurrentYValue = p.Y;
                        propPoint3D.CurrentZValue = p.Z;

                        kfp = (CustomPoint3DKeyFrame)propPoint3D.OnCurrentValueChange();
                        if (kfp != null && recordUserActions)
                        {
                            // New keyframe was created
                            kfp.IsSelected = true;

                            LfsRecordController.RecordUserAction(new UserActionAddKeyframe()
                                {
                                    LayerIndex = kfp.ParentLayer.LayerIndex,
                                    PropertyIndex = kfp.ParentProperty.PropertyIndex,
                                    KeyFrame = kfp.DataStructure()
                                },
                                true);
                        }
                    }

                    l.SetCurrentTransformMatrix();
                }
                else if (l.LayerType == CompLayerTypes.FreeCamera &&
                    ((CompLayerFreeCamera)l).IsActiveCamera)
                {
                    CompLayerFreeCamera cl = (CompLayerFreeCamera)l;
                    mat = cl.GetParentedTransformMatrixAtFrame(LfsRecordController.FrameIndex, false);
                    mat.Invert();

                    p = mat.Transform(new Point3D(cam.PosX, cam.PosY, cam.PosZ));
                    propPoint3D = (CompLayerPropertyPoint3D)cl.Properties[(int)CompLayerFreeCamera.PropertyTypes.Position];
                    if (propPoint3D.CurrentXValue != p.X ||
                        propPoint3D.CurrentYValue != p.Y ||
                        propPoint3D.CurrentZValue != p.Z)
                    {
                        propPoint3D.CurrentXValue = p.X;
                        propPoint3D.CurrentYValue = p.Y;
                        propPoint3D.CurrentZValue = p.Z;

                        kfp = (CustomPoint3DKeyFrame)propPoint3D.OnCurrentValueChange();
                        if (kfp != null && recordUserActions)
                        {
                            // New keyframe was created
                            kfp.IsSelected = true;

                            LfsRecordController.RecordUserAction(new UserActionAddKeyframe()
                                {
                                    LayerIndex = kfp.ParentLayer.LayerIndex,
                                    PropertyIndex = kfp.ParentProperty.PropertyIndex,
                                    KeyFrame = kfp.DataStructure()
                                },
                                true);
                        }
                    }

                    heading = cam.Heading;
                    pitch = -cam.Pitch;

                    //Debug.WriteLine("Heading : {0}, {1}, {2}", heading, cam.Heading, ((CompLayerPropertyDouble)cl.Properties[(int)CompLayerFreeCamera.PropertyTypes.Heading]).CurrentValue);
                    propDouble = (CompLayerPropertyDouble)cl.Properties[(int)CompLayerFreeCamera.PropertyTypes.Heading];
                    if (propDouble.CurrentValue != heading)
                    {
                        propDouble.CurrentValue = heading;

                        kfd = (CustomDoubleKeyFrame)propDouble.OnCurrentValueChange();
                        if (kfd != null && recordUserActions)
                        {
                            // New keyframe was created
                            kfd.IsSelected = true;

                            LfsRecordController.RecordUserAction(new UserActionAddKeyframe()
                                {
                                    LayerIndex = kfd.ParentLayer.LayerIndex,
                                    PropertyIndex = kfd.ParentProperty.PropertyIndex,
                                    KeyFrame = kfd.DataStructure()
                                },
                                true);
                        }
                    }

                    propDouble = (CompLayerPropertyDouble)cl.Properties[(int)CompLayerFreeCamera.PropertyTypes.Pitch];
                    if (propDouble.CurrentValue != pitch)
                    {
                        propDouble.CurrentValue = pitch;

                        kfd = (CustomDoubleKeyFrame)propDouble.OnCurrentValueChange();
                        if (kfd != null && recordUserActions)
                        {
                            // New keyframe was created
                            kfd.IsSelected = true;

                            LfsRecordController.RecordUserAction(new UserActionAddKeyframe()
                                {
                                    LayerIndex = kfd.ParentLayer.LayerIndex,
                                    PropertyIndex = kfd.ParentProperty.PropertyIndex,
                                    KeyFrame = kfd.DataStructure()
                                },
                                true);
                        }
                    }

                    l.SetCurrentTransformMatrix();
                }
            }
        }

        public CameraTypes GetActiveCameraType()
        {
            foreach (CompLayerBase l in Layers)
            {
                if (l.LayerType == CompLayerTypes.LookCamera &&
                    ((CompLayerLookCamera)l).IsActiveCamera)
                {
                    return CameraTypes.LookCamera;
                }
                else if (l.LayerType == CompLayerTypes.FreeCamera &&
                    ((CompLayerFreeCamera)l).IsActiveCamera)
                {
                    return CameraTypes.FreeCamera;
                }
            }

            return CameraTypes.NoCamera;
        }

        public CompLayerBase GetActiveCameraLayer()
        {
            foreach (CompLayerBase l in Layers)
            {
                if ((l.LayerType == CompLayerTypes.LookCamera ||
                     l.LayerType == CompLayerTypes.FreeCamera) &&
                    l.IsActiveCamera)
                {
                    return l;
                }
            }

            return null;
        }

        public LfsCamera GetActiveCameraAtIndex(int frame)
        {
            Matrix3D mat, preHeading, prePitch;
            Point3D lookAt = new Point3D();
            double pitch, roll, heading;
            LfsCamera cam;

            foreach (CompLayerBase l in Layers)
            {
                if ((l.LayerType == CompLayerTypes.LookCamera ||
                     l.LayerType == CompLayerTypes.FreeCamera) &&
                    l.IsActiveCamera)
                {
                    mat = l.GetParentedTransformMatrixAtFrame(frame);

                    // Extract rotation values
                    heading = Math.Atan2(mat.M21, mat.M22);
                    preHeading = Matrix3D.Multiply(mat, MathHelper.RotateZMatrix3D(heading));

                    pitch = Math.Atan2(-preHeading.M23, preHeading.M22);
                    prePitch = Matrix3D.Multiply(preHeading, MathHelper.RotateXMatrix3D(pitch));

                    roll = Math.Atan2(prePitch.M13, prePitch.M11);

                    if (l.LayerType == CompLayerTypes.LookCamera)
                    {
                        // populate lookat value, used for moving camera in 3d view
                        lookAt = mat.Transform(new Point3D(0, ((CompLayerLookCamera)l).GetLookAtDistance(frame), 0));
                        //Debug.WriteLine("Look at : {0}, {1}, {2}", lookAt.X, lookAt.Y, lookAt.Z);

                        cam = new LfsCamera(CameraTypes.LookCamera);
                        cam.PosX = mat.OffsetX;
                        cam.PosY = mat.OffsetY;
                        cam.PosZ = mat.OffsetZ;

                        if (l.HasParentLayer && l.Properties[(int)CompLayerLookCamera.PropertyTypes.LookAt].HasBoundProperty && l.Properties[(int)CompLayerLookCamera.PropertyTypes.LookAt].BoundPropertyIgnoreParent)
                        {
                            cam.LookAt = (Point3D)l.Properties[(int)CompLayerLookCamera.PropertyTypes.LookAt].GetValueAtFrame(frame);
                            Point3D poiNorm = new Point3D(
                                cam.LookAt.X - cam.PosX,
                                cam.LookAt.Y - cam.PosY,
                                cam.LookAt.Z - cam.PosZ
                                );
                            cam.Heading = (Math.Atan2(poiNorm.Y, poiNorm.X) - Math.PI / 2) * MathHelper.RadDegMult;
                            cam.Pitch = -Math.Atan2(poiNorm.Z, Math.Sqrt(poiNorm.X * poiNorm.X + poiNorm.Y * poiNorm.Y)) * MathHelper.RadDegMult;
                        }
                        else
                        {
                            cam.LookAt = lookAt;
                            cam.Heading = -heading * MathHelper.RadDegMult;
                            cam.Pitch = pitch * MathHelper.RadDegMult;
                        }

                        cam.Roll = -roll * MathHelper.RadDegMult;
                        cam.FOV = (double)l.Properties[(int)CompLayerLookCamera.PropertyTypes.Fov].GetValueAtFrame(frame);
                    }
                    else
                    {
                        CompLayerFreeCamera cl = (CompLayerFreeCamera)l;
                        cam = new LfsCamera(CameraTypes.FreeCamera);
                        cam.PosX = mat.OffsetX;
                        cam.PosY = mat.OffsetY;
                        cam.PosZ = mat.OffsetZ;
                        cam.LookAt = lookAt;
                        //cam.Heading = ((CompLayerPropertyDouble)cl.Properties[(int)CompLayerFreeCamera.PropertyTypes.Heading]).CurrentValue;
                        //cam.Pitch = -((CompLayerPropertyDouble)cl.Properties[(int)CompLayerFreeCamera.PropertyTypes.Pitch]).CurrentValue;
                        //cam.Roll = ((CompLayerPropertyDouble)cl.Properties[(int)CompLayerFreeCamera.PropertyTypes.Roll]).CurrentValue;
                        cam.Heading = -heading * MathHelper.RadDegMult;
                        cam.Pitch = pitch * MathHelper.RadDegMult;
                        cam.Roll = roll * MathHelper.RadDegMult;
                        cam.FOV = (double)l.Properties[(int)CompLayerFreeCamera.PropertyTypes.Fov].GetValueAtFrame(frame);
                    }

                    return cam;
                }
            }
            return null;
        }

        public int GetFirstSelectedFrameNum()
        {
            int index = int.MaxValue;

            foreach (CompLayerBase l in Layers)
            {
                foreach (CompLayerPropertyBase prop in l.Properties)
                {
                    foreach (CustomKeyFrame kf in prop.KeyFrames)
                    {
                        if (kf.IsSelected && kf.FrameNum < index)
                        {
                            index = kf.FrameNum;
                            break;
                        }
                    }
                }
            }

            foreach (AudioLayer l in AudioLayers)
            {
                foreach (AudioRegion ar in l.AudioRegions)
                {
                    if (ar.IsSelected && ar.TimeOffset * FPS < index)
                    {
                        index = (int)Math.Round(ar.TimeOffset * FPS);
                        break;
                    }
                }
            }

            return index;
        }

        public void CutSelectedKeyFrames()
        {
            CopySelectedKeyFrames(true);
        }
        public void CopySelectedKeyFrames()
        {
            CopySelectedKeyFrames(false);
        }
        public void CopySelectedKeyFrames(bool cut)
        {
            KeyFrameCopyBuffer.Clear();
            foreach (CompLayerBase l in Layers)
            {
                foreach (CompLayerPropertyBase prop in l.Properties)
                {
                    foreach (CustomKeyFrame kf in prop.KeyFrames)
                    {
                        if (kf.IsSelected)
                        {
                            KeyFrameCopyBuffer.Add(new KeyFrameCopy()
                                {
                                    LayerIndex = l.LayerIndex,
                                    PropertyIndex = prop.PropertyIndex,
                                    KeyFrame = kf.DataStructure()
                                });
                        }
                    }
                }
            }

            if (cut)
            {
                SubmitUserActionDeleteSelectedKeyFrames();
                DeleteSelectedKeyFrames();
            }

            //Debug.WriteLine("Copied {0} keyframes", KeyFrameCopyBuffer.Count);
        }

        public void PasteSelectedKeyFrames()
        {
            if (KeyFrameCopyBuffer.Count == 0)
                return;

            DeselectAll();

            CustomKeyFrame kf;
            int frameOffset = LfsRecordController.FrameIndex - KeyFrameCopyBuffer[0].KeyFrame.FrameNum;
            foreach (KeyFrameCopy kfc in KeyFrameCopyBuffer)
            {
                kfc.KeyFrame.FrameNum += frameOffset;
                kf = Layers[kfc.LayerIndex].Properties[kfc.PropertyIndex].ReinstateKeyFrame(kfc.KeyFrame);
                LfsRecordController.RecordUserAction(new UserActionAddKeyframe()
                    {
                        LayerIndex = kfc.LayerIndex,
                        PropertyIndex = kfc.PropertyIndex,
                        KeyFrame = kf.DataStructure()
                    });
            }

            DeleteOverlappingKeyFrames();
        }

        public void MoveSelectedKeyFrames(int moveOffset)
        {
            bool hasMoved;
            foreach (CompLayerBase l in Layers)
            {
                foreach (CompLayerPropertyBase prop in l.Properties)
                {
                    hasMoved = false;
                    foreach (CustomKeyFrame kf in prop.KeyFrames)
                    {
                        if (kf.IsSelected)
                        {
                            kf.FrameNum += moveOffset;
                            hasMoved = true;
                        }
                    }

                    if (hasMoved)
                    {
                        prop.SortKeyFrames();
                        prop.RewriteKeyFrameIndices();
                        prop.SetHasPreFrames();
                        prop.SetHasPostFrames();
                        prop.SetHasCurrentFrame();
                        if (prop.PropertyValueType == LayerPropertyValueTypes.Point3D)
                        {
                            ((CompLayerPropertyPoint3D)prop).populatePoint3DCache();
                        }
                    }
                }
            }
        }

        public void MoveSelectedAudioRegions(double moveOffset)
        {
            bool hasMoved;
            lock (_audioLock)
            {
                foreach (AudioLayer l in AudioLayers)
                {
                    hasMoved = false;
                    foreach (AudioRegion ar in l.AudioRegions)
                    {
                        if (ar.IsSelected)
                        {
                            ar.TimeOffset += moveOffset;
                            if (ar.AudioStream != null)
                            {
                                ar.PlaybackPtr = (long)((LfsRecordController.FrameIndex / (double)FPS - ar.TimeOffset) * (ar.AudioStream.Format.nBlockAlign * LfsRecordController.Mixer.SampleRate));
                                ar.PlaybackPtr -= ar.PlaybackPtr % ar.AudioStream.Format.nBlockAlign;
                            }
                            hasMoved = true;
                        }
                    }
                    if (hasMoved)
                    {
                        l.SortRegions();
                    }
                }
            }
        }

        public void TranslateSelectedKeyFrames(double xOffset, double yOffset, double zOffset)
        {
            bool hasMoved;
            foreach (CompLayerBase l in Layers)
            {
                foreach (CompLayerPropertyBase prop in l.Properties)
                {
                    if (!prop.IsAnimated ||
                        prop.PropertyValueType == LayerPropertyValueTypes.Double ||
                        prop.PropertyValueType == LayerPropertyValueTypes.String ||
                        !((CompLayerPropertyPoint3D)prop).HasAnimationPath)
                        continue;

                    hasMoved = false;
                    foreach (CustomPoint3DKeyFrame kf in ((CompLayerPropertyPoint3D)prop).KeyFrames)
                    {
                        if (kf.IsSelected)
                        {
                            kf.Value = new Point3D(kf.Value.X - xOffset, kf.Value.Y - yOffset, kf.Value.Z - zOffset);
                            ((CompLayerPropertyPoint3D)prop).AutoBezierCore(kf);
                            hasMoved = true;
                        }
                    }

                    if (hasMoved)
                        ((CompLayerPropertyPoint3D)prop).populatePoint3DCache();
                }
            }

            ServicesFactory.EventService.GetEvent<CompLayerPropertyChangedEvent>().Publish(new CompLayerPropertyChangedEventArgs() { Sender = this, PropertyType = "Value", UpdateCurrentValues = true });
        }

        public bool PaceSelectedKeyFrames()
        {
            if (!VerifySelectionIsContiguous())
                return false;

            int firstIndex, lastIndex, numFrames;
            double selectionDistance, previousDistance, newFrameNum;
            foreach (CompLayerBase l in Layers)
            {
                foreach (CompLayerPropertyBase prop in l.Properties)
                {
                    firstIndex = lastIndex = -1;
                    selectionDistance = previousDistance = 0;

                    // Find first and last selected keyframe index
                    for (int c = 0; c < prop.KeyFrames.Count; c++)
                    {
                        if (prop.KeyFrames[c].IsSelected)
                        {
                            selectionDistance += previousDistance;
                            if (firstIndex == -1)
                                firstIndex = c;
                            lastIndex = c;
                            previousDistance = prop.KeyFrames[c].DistanceToNextKeyFrame;
                        }
                        else if (firstIndex > -1)
                            break;
                    }

                    // Nothing to do on this property?
                    if (firstIndex == -1 || firstIndex == lastIndex)
                        continue;

                    // Pace the selected keyframes
                    numFrames = prop.KeyFrames[lastIndex].FrameNum - prop.KeyFrames[firstIndex].FrameNum;

                    newFrameNum = prop.KeyFrames[firstIndex].FrameNum;
                    for (int c = firstIndex + 1; c <= lastIndex; c++)
                    {
                        newFrameNum += prop.KeyFrames[c - 1].DistanceToNextKeyFrame / selectionDistance * numFrames;

                        // Record user actions
                        LfsRecordController.RecordUserAction(new UserActionMoveKeyFrame()
                            {
                                LayerIndex = l.LayerIndex,
                                PropertyIndex = prop.PropertyIndex,
                                UniqueID = prop.KeyFrames[c].UniqueID,
                                FrameOffset = (int)newFrameNum - prop.KeyFrames[c].FrameNum
                            });

                        prop.KeyFrames[c].FrameNum = (int)newFrameNum;

                        //Debug.WriteLine("FrameNum : " + prop.KeyFrames[c].FrameNum);
                    }

                    if (prop.PropertyValueType == LayerPropertyValueTypes.Point3D && ((CompLayerPropertyPoint3D)prop).HasAnimationPath)
                    {
                        ((CompLayerPropertyPoint3D)prop).populatePoint3DCache();
                    }
                }
            }

            return true;
        }

        public void ReloadSelectedMediaObjects()
        {
            foreach (MediaBase mb in MediaObjects)
            {
                if (mb.IsSelected)
                {
                    mb.ReloadFile();
                }
            }
        }

        public void RenameMedia(string oldFilePath, string newFilePath)
        {
            // Rename media files that are in use (in the layers)
            foreach (CompLayerBase l in Layers)
            {
                if (l.LayerType == CompLayerTypes.ReplayData)
                {
                    if (l.Properties[(int)CompLayerReplayData.PropertyTypes.DataSelection].KeyFrames.Count == 0)
                    {
                        RDIdent rdIdent = ((CompLayerPropertyRDIdent)l.Properties[(int)CompLayerReplayData.PropertyTypes.DataSelection]).CurrentValue;
                        if (rdIdent.ReplayFilePath == oldFilePath)
                        {
                            ((CompLayerPropertyRDIdent)l.Properties[(int)CompLayerReplayData.PropertyTypes.DataSelection]).CurrentValue = 
                                new RDIdent(newFilePath, rdIdent.PlayerName, rdIdent.PlayerIndex);
                        }
                    }
                    else
                    {
                        foreach (CustomRDKeyFrame kfr in l.Properties[(int)CompLayerReplayData.PropertyTypes.DataSelection].KeyFrames)
                        {
                            if (kfr.Value.ReplayFilePath == oldFilePath)
                            {
                                kfr.Value = new RDIdent(newFilePath, kfr.Value.PlayerName, kfr.Value.PlayerIndex);
                            }
                        }
                    }
                }
            }


            lock (_audioLock)
            {
                foreach (AudioLayer l in AudioLayers)
                {
                    foreach (AudioRegion ar in l.AudioRegions)
                    {
                        if (ar.AudioFilePath == oldFilePath)
                        {
                            ar.AudioFilePath = newFilePath;
                        }
                    }
                }
            }
        }

        public void ReloadMediaObject(string filePath)
        {
            int mediaIndex = HasMediaFile(filePath);
            if (mediaIndex == -1)
                return;

            ReloadMediaObject(MediaObjects[mediaIndex]);
        }

        public void ReloadMediaObject(MediaBase mb)
        {
            mb.ReloadFile();

            if (mb.MediaType == MediaTypes.Audio)
            {
                lock (_audioLock)
                {
                    foreach (AudioLayer l in AudioLayers)
                    {
                        foreach (AudioRegion ar in l.AudioRegions)
                        {
                            if (ar.AudioFilePath == mb.FilePath)
                            {
                                ar.ReattachAudioStream();
                            }
                        }
                    }
                }
            }
        }

        public void ReattachBrokenStreams(MediaAudio ma)
        {
            lock (_audioLock)
            {
                foreach (AudioLayer l in AudioLayers)
                {
                    foreach (AudioRegion ar in l.AudioRegions)
                    {
                        if (ar.AudioFilePath == ma.FilePath)
                        {
                            if (ar.AudioStream == null)
                            {
                                ar.ReattachAudioStream();
                                ar.PlaybackPtr = (long)((LfsRecordController.FrameIndex / (double)FPS - ar.TimeOffset) * (ar.AudioStream.Format.nBlockAlign * LfsRecordController.Mixer.SampleRate));
                                ar.PlaybackPtr -= ar.PlaybackPtr % ar.AudioStream.Format.nBlockAlign;
                            }
                        }
                    }
                }
            }
        }

        public void FPSChangeStretch(byte oldFPS, byte newFPS)
        {
            double frameMult = (double)newFPS / (double)oldFPS;
            foreach (CompLayerBase l in Layers)
            {
                foreach (CompLayerPropertyBase prop in l.Properties)
                {
                    foreach (CustomKeyFrame kf in prop.KeyFrames)
                    {
                        kf.FrameNum = (int)Math.Round(kf.FrameNum * frameMult);
                        //Debug.WriteLine("New keyframe framenum : {0} - mult : {1}", kf.FrameNum, frameMult);
                    }

                    if (prop.PropertyValueType == LayerPropertyValueTypes.Point3D && ((CompLayerPropertyPoint3D)prop).HasAnimationPath)
                    {
                        ((CompLayerPropertyPoint3D)prop).populatePoint3DCache();
                    }
                }
            }
        }

        public bool VerifySelectionIsContiguous()
        {
            int numSelected = 0, previousSelectedIndex;
            CompLayerBase layer;
            CompLayerPropertyBase prop;

            for (int a = 0; a < Layers.Count; a++)
            {
                layer = Layers[a];
                for (int b = 0; b < layer.Properties.Count; b++)
                {
                    prop = layer.Properties[b];
                    previousSelectedIndex = -1;

                    for (int c = 0; c < prop.KeyFrames.Count; c++)
                    {
                        if (prop.KeyFrames[c].IsSelected)
                        {
                            // False if selected keyframes are not contiguous
                            if (previousSelectedIndex > -1 && previousSelectedIndex != c - 1)
                                return false;

                            previousSelectedIndex = c;
                            numSelected++;
                        }
                    }
                }
            }

            return (numSelected < 2) ? false : true;
        }

        public bool HasSelectedLayers()
        {
            foreach (CompLayerBase l in Layers)
            {
                if (l.IsSelected)
                    return true;
            }
            foreach (AudioLayer l in AudioLayers)
            {
                if (l.IsSelected)
                    return true;
            }
            return false;
        }

        public int HasMediaFile(string filePath)
        {
            for (int a = 0; a < MediaObjects.Count; a++)
            {
                if (MediaObjects[a].FilePath == filePath)
                    return a;
            }
            return -1;
        }

        public void DeleteMediaObject(string filePath)
        {
            for (int a = 0; a < MediaObjects.Count; a++)
            {
                if (MediaObjects[a].FilePath == filePath)
                {
                    MediaObjects[a].Dispose();
                    if (MediaObjects[a].MediaType == MediaTypes.Audio)
                        InvalidateAudioRegionStreams((MediaAudio)MediaObjects[a]);
                    MediaObjects.RemoveAt(a);
                    break;
                }
            }

            // Invalidate audio regions that used this media object (audio)
            
        }

        private void InvalidateAudioRegionStreams(MediaAudio ma)
        {
            foreach (AudioLayer l in AudioLayers)
            {
                foreach (AudioRegion ar in l.AudioRegions)
                {
                    if (ar.AudioFilePath == ma.FilePath)
                    {
                        ar.AudioStream = null;
                    }
                }
            }
        }

        public void DeleteOverlappingKeyFrames()
        {
            int a;
            CustomKeyFrame kf, kfPrev;
            bool haveRemoved;

            foreach (CompLayerBase l in Layers)
            {
                foreach (CompLayerPropertyBase prop in l.Properties)
                {
                    kfPrev = null;
                    haveRemoved = false;
                    for (a = 0; a < prop.KeyFrames.Count; a++)
                    {
                        kf = prop.KeyFrames[a];
                        if (a == 0)
                        {
                            kfPrev = kf;
                            continue;
                        }

                        if (kfPrev.FrameNum == kf.FrameNum)
                        {
                            LfsRecordController.RecordUserAction(new UserActionDeleteKeyFrame()
                                {
                                    LayerIndex = l.LayerIndex,
                                    PropertyIndex = prop.PropertyIndex,
                                    KeyFrame = kfPrev.DataStructure()
                                });
                            prop.KeyFrames.RemoveAt(--a);
                            haveRemoved = true;
                        }

                        kfPrev = kf;
                    }

                    if (haveRemoved)
                    {
                        prop.RewriteKeyFrameIndices();
                        prop.SetHasPreFrames();
                        prop.SetHasPostFrames();
                        prop.SetHasCurrentFrame();
                        if (prop.PropertyValueType == LayerPropertyValueTypes.Point3D)
                        {
                            ((CompLayerPropertyPoint3D)prop).populatePoint3DCache();
                        }
                    }
                }
            }
        }

        public void DeselectAll()
        {
            DeselectAllKeyFrames();
            DeselectAllProperties();
            DeselectAllAudioRegions();
            DeselectAllLayers();
        }

        public void DeselectAllMedia()
        {
            foreach (MediaBase mb in MediaObjects)
            {
                mb.IsSelected = false;
            }
        }

        public void DeselectAllLayers()
        {
            foreach (CompLayerBase l in Layers)
            {
                l.IsSelected = false;
            }
            lock (_audioLock)
            {
                foreach (AudioLayer l in AudioLayers)
                {
                    l.IsSelected = false;
                }
            }
        }

        public void DeselectAllProperties()
        {
            foreach (CompLayerBase l in Layers)
            {
                foreach (CompLayerPropertyBase prop in l.Properties)
                {
                    if (prop.IsSelected)
                        prop.IsSelected = false;
                }
            }
            lock (_audioLock)
            {
                foreach (AudioLayer l in AudioLayers)
                {
                    foreach (AudioLayerPropertyBase prop in l.Properties)
                    {
                        if (prop.IsSelected)
                            prop.IsSelected = false;
                    }
                }
            }
        }

        public void DeselectAllKeyFrames()
        {
            foreach (CompLayerBase l in Layers)
            {
                foreach (CompLayerPropertyBase prop in l.Properties)
                {
                    if (prop.HasSelectedKeyFrames)
                    {
                        foreach (CustomKeyFrame kf in prop.KeyFrames)
                        {
                            if (kf.IsSelected)
                                kf.IsSelected = false;
                        }
                        prop.HasSelectedKeyFrames = false;
                    }
                }
            }
        }

        public void DeselectAllAudioRegions()
        {
            lock (_audioLock)
            {
                foreach (AudioLayer l in AudioLayers)
                {
                    foreach (AudioRegion ar in l.AudioRegions)
                    {
                        if (ar.IsSelected)
                            ar.IsSelected = false;
                    }
                }
            }
        }

        public void DeselectActiveCamera()
        {
            foreach (CompLayerBase l in Layers)
            {
                l.IsActiveCamera = false;
            }
        }

        public void SyncIsSelectedBackup()
        {
            foreach (CompLayerBase l in Layers)
            {
                foreach (CompLayerPropertyBase prop in l.Properties)
                {
                    foreach (CustomKeyFrame kf in prop.KeyFrames)
                    {
                        kf.SelectionID = 0;
                        kf.IsSelectedBackup = kf.IsSelected;
                    }
                }
            }
        }

        public void ResetKeyFrameSelectionNotHit(int selectionID)
        {
            foreach (CompLayerBase l in Layers)
            {
                foreach (CompLayerPropertyBase prop in l.Properties)
                {
                    foreach (CustomKeyFrame kf in prop.KeyFrames)
                    {
                        if (kf.SelectionID < selectionID && kf.IsSelected != kf.IsSelectedBackup)
                        {
                            kf.IsSelected = kf.IsSelectedBackup;
                        }
                    }
                }
            }
        }

        public void SubmitUserActionDeleteSelectedKeyFrames()
        {
            foreach (CompLayerBase l in Layers)
            {
                foreach (CompLayerPropertyBase prop in l.Properties)
                {
                    if (!prop.HasSelectedKeyFrames)
                        continue;

                    foreach (CustomKeyFrame kf in prop.KeyFrames)
                    {
                        if (!kf.IsSelected)
                            continue;

                        LfsRecordController.RecordUserAction(new UserActionDeleteKeyFrame()
                        {
                            LayerIndex = l.LayerIndex,
                            PropertyIndex = prop.PropertyIndex,
                            KeyFrame = kf.DataStructure()
                        });
                    }
                }
            }
        }

        public void SubmitUserActionDeleteSelectedAudioRegions()
        {
            foreach (AudioLayer l in AudioLayers)
            {
                foreach (AudioRegion ar in l.AudioRegions)
                {
                    if (ar.IsSelected)
                    {
                        LfsRecordController.RecordUserAction(new UserActionDeleteAudioRegion()
                        {
                            LayerIndex = l.LayerIndex,
                            RegionData = ar.DataStructure()
                        });
                    }
                }
            }
        }

        public void SubmitUserActionMoveSelectedKeyFrames(int frameOffset)
        {
            foreach (CompLayerBase l in Layers)
            {
                foreach (CompLayerPropertyBase prop in l.Properties)
                {
                    if (!prop.HasSelectedKeyFrames)
                        continue;

                    foreach (CustomKeyFrame kf in prop.KeyFrames)
                    {
                        if (!kf.IsSelected)
                            continue;

                        LfsRecordController.RecordUserAction(new UserActionMoveKeyFrame()
                        {
                            LayerIndex = l.LayerIndex,
                            PropertyIndex = prop.PropertyIndex,
                            UniqueID = kf.UniqueID,
                            FrameOffset = frameOffset
                        });
                    }
                }
            }
        }

        public void SubmitUserActionMoveSelectedAudioRegions(double moveOffset)
        {
            foreach (AudioLayer l in AudioLayers)
            {
                foreach (AudioRegion ar in l.AudioRegions)
                {
                    if (ar.IsSelected)
                    {
                        LfsRecordController.RecordUserAction(new UserActionMoveAudioRegion()
                        {
                            LayerIndex = l.LayerIndex,
                            RegionUniqueID = ar.UniqueID,
                            MoveOffset = moveOffset
                        });
                    }
                }
            }
        }

        public void SubmitUserActionSelectedKeyFramePoint3DValueChange(Vector3D offsetVec)
        {
            foreach (CompLayerBase l in Layers)
            {
                foreach (CompLayerPropertyBase prop in l.Properties)
                {
                    if (!prop.HasSelectedKeyFrames)
                        continue;

                    foreach (CustomKeyFrame kf in prop.KeyFrames)
                    {
                        if (!kf.IsSelected)
                            continue;

                        LfsRecordController.RecordUserAction(new UserActionKeyFramePoint3DValueChanged()
                        {
                            LayerIndex = kf.ParentLayer.LayerIndex,
                            PropertyIndex = kf.ParentProperty.PropertyIndex,
                            KeyFrameIndex = kf.KeyFrameIndex,
                            ValueOffset = offsetVec
                        });
                    }
                }
            }
        }

        public ProjectData DataStructure()
        {
            Debug.WriteLine("Creating Project Datastructure");

            // Create our struct
            ProjectData projectData = new ProjectData()
            {
                ProjectName = ProjectName,
                ProjectFilePath = ProjectFilePath,
                ProjectLength = ProjectLength,
                FPS = FPS,
                InSimIP = InSimIP,
                InSimPort = InSimPort,
                InSimAdminPassword = InSimAdminPassword,
                RecordFullScreenOverride = RecordFullScreenOverride,
                RecordStopHotkey = RecordStopHotkey,
                RecordingWidth = RecordingWidth,
                RecordingHeight = RecordingHeight,
                RecordHide2D = RecordHide2D,
                Move3DAxesInvert = Move3DAxesInvert,
                NumLayers = Layers.Count,
                Layers = new LayerData[Layers.Count],
                DefaultInterpolationType = DefaultInterpolationType
            };

            // Populate the layers
            for (int a = 0; a < Layers.Count; a++)
            {
                projectData.Layers[a] = Layers[a].DataStructure();
            }

            return projectData;
        }
    }
}
